package etxt2db.mappings;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * <p>
 * Mapper between annotation keys and annotation types specified 
 * on a configuration file.
 * </p>
 * 
 * <p>
 * The configuration file should have the following syntax per line:
 * 
 * <br>
 * <br>
 * 
 * <code>&ltname of the key&gt &ltfully qualified name of the class&gt &ltlist of methods to capture from the class&gt</code>
 * 
 * <br>
 * <br>
 * 
 * Example:
 * <code>
 * <br>
 * <br>
 * Token org.u_compare.shared.syntactic.Token
 * <br>
 * POSToken org.u_compare.shared.syntactic.POSToken	getPosString
 * </code>
 * </p>
 * 
 * @author Gonçalo Simões
 * @author Rui Lageira
 */
public class AnnotationToKeyMapper {
	
	private Map<String,List<String>> mappings = new HashMap<String, List<String>>();
	private Map<String,String> mappingsReversed = new HashMap<String,String>();
	private List<String> linesFile = new ArrayList<String>();

	/**
	 * Constructs the mapper using the default configuration file
	 * <code>config/type_mappings.config</code> specifying the mapping between 
	 * annotation keys and types.
	 */
	public AnnotationToKeyMapper(){
		loadConfigurationFile("config/type_mappings.config");
	}
	
	/**
	 * Constructs the mapper using a provided configuration file.
	 * 
	 * @param path	the path to the configuration file
	 */
	public AnnotationToKeyMapper(String path){
		loadConfigurationFile(path);
	}

	/**
	 * Constructs the mapper using a provided list of line contents.
	 * 
	 * @param mappingsContentList the list of line contents
	 */
	public AnnotationToKeyMapper(List<String> mappingsContentList) {
		int size = mappingsContentList.size();
		for(int i=0; i<size; i++){
			processEntry(mappingsContentList.get(i), i);
		}
	}

	/**
	 * Obtains the list of annotation keys mapping the types of 
	 * a given annotation object.
	 * <p>
	 * @param obj	the annotation object to get the keys from
	 * @return		the list of annotation keys
	 * @throws ClassNotFoundException	if some class in the map can't be located
	 */
	public List<String> getKey(Object obj) throws ClassNotFoundException{
		List<Class<?>> possibleKeys = new ArrayList<Class<?>>();
		for(Entry<String,List<String>> entry : mappings.entrySet()){
			Class<?> c = Class.forName(entry.getKey());
			if(c.isInstance(obj)){
				possibleKeys.add(c);
			}
		}

		while(true){
			List<Class<?>> classesToRemove = new ArrayList<Class<?>>();
			int possibleKeysSize = possibleKeys.size();
			for(int i=0; i<possibleKeysSize; i++){
				Class<?> c1 = possibleKeys.get(i);
				for(int j=0; j<possibleKeysSize; j++){
					Class<?> c2 = possibleKeys.get(j);
					if(i!=j){
						if(c1.isAssignableFrom(c2)){
							classesToRemove.add(c1);
						}
					}
				}
			}
			if(classesToRemove.size()==0){
				break;
			}else{
				for(Class<?> c : classesToRemove){
					possibleKeys.remove(c);
				}
			}
		}


		List<String> result = new ArrayList<String>();
		for(Class<?> c : possibleKeys){
			result.add(mappings.get(c.getName()).get(0));
		}

		return result;
	}

	/**
	 * Returns the list of objects returned from invoking a list of methods 
	 * belonging to the annotation class correspondent to a provided annotation key, 
	 * as specified in the configuration file.
	 * This list of objects distinguishes the particular type of the annotation object 
	 * provided.
	 * <p>
	 * @param obj	the annotation object from which to invoke the methods
	 * @param key	the annotation key corresponding to an annotation class
	 * @return	the list of objects returned from invoking the list of methods 
	 * belonging to the annotation class correspondent to the provided annotation key
	 * @throws SecurityException			if a problem getting a {@link Method} object occurs
	 * @throws NoSuchMethodException		if a method is not found
	 * @throws IllegalArgumentException		if an illegal or inappropriate argument is passed to a method
	 * @throws IllegalAccessException		if a problem with accessing a method occurs
	 * @throws InvocationTargetException	if an exception is thrown by an invoked method or constructor
	 * @throws ClassNotFoundException		if the annotation class can't be located
	 */
	public List<Object> getAnnotations(Object obj, String key) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, ClassNotFoundException{
		String cla = mappingsReversed.get(key);
		Class<?> c = Class.forName(cla);
		List<String> keyAndMethods = mappings.get(cla);

		int numKeyAndMethods = keyAndMethods.size();
		List<Object> result = new ArrayList<Object>();
		//result.add(key);
		for(int i=1; i<numKeyAndMethods; i++){
			Method m = c.getMethod(keyAndMethods.get(i));
			result.add(m.invoke(obj));
		}
		return result;
	}

	/**
	 * Returns the fully qualified name of the class mapped by
	 * a key.
	 * 
	 * @param key	the key mapping the class
	 * @return the fully qualified name of the mapped class
	 */
	public String getPath(String key) {
		return mappingsReversed.get(key);
	}
	
	private void loadConfigurationFile(String path){
		try{
			FileInputStream fstream = new FileInputStream(path);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			int i=1;
			while ((strLine = br.readLine()) != null)   {
				linesFile.add(strLine);
				processEntry(strLine,i);
				i++;
			}
			in.close();
		}catch (Exception e){//Catch exception if any
			System.err.println("Error: " + e.getMessage());
			System.exit(1);
		}
	}
	
	private void processEntry(String strLine, int line){
		String[] entry = strLine.split("\\s+");
		if(entry.length<2){
			throw new ArrayIndexOutOfBoundsException("The entry of the mappings file in line " + line + " does not have all the necessary inputs: KEY CLASS");
		}else{
			addEntry(entry);
		}
	}

	private void addEntry(String[] entry){
		String key = entry[0];
		String path = entry[1];
		
		List<String> listKey = new ArrayList<String>();
		listKey.add(key);
		for(int i=2; i<entry.length; i++){
			listKey.add(entry[i]);
		}
		mappings.put(path, listKey);
		mappingsReversed.put(key, path);
	}

	public List<String> getContent() {
		return linesFile;
	}
}